package com.example.musicplayer.calculator;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

public class Calculate {
    private static final int DEFAULT_DECIMAL_PLACES = 6;
    private static String sExp;
    private static int sDecimalPlaces;
    private static boolean sSetDecimalPlaces = true;
    private static List<String> sData;
    private static Stack<String> sStack;
    private static LinkedList<String> sQueue;

    public static String calculate(String expression) throws CalculateException {
        sSetDecimalPlaces = false;
        return calculate(expression, DEFAULT_DECIMAL_PLACES);
    }

    public static String calculate(String expression, int decimalPlaces) throws CalculateException {
        init(expression, decimalPlaces);
        if (sExp.length() == 0) {
            throw new CalculateException(CalculateException.ERROR);
        }
        changeToStringList();
        //check if expression is valid
        checkNumber();
        checkBracket();
        checkOperator();
        //change to postfix expression
        changeToPostfixExp();
        //get the result
        String result = getResult();
        destroy();
        return result;
    }

    /**
     * analyze the input string as string array list
     *
     * @throws CalculateException invalid character
     */
    private static void changeToStringList() throws CalculateException {
        int i = 0, j = 0, index = 0;
        while (i < sExp.length()) {
            if (isCharNumber(sExp.charAt(i))) {
                j = i + 1;
                while (j < sExp.length() && isCharNumber(sExp.charAt(j))) {
                    j++;
                }
                sData.add(sExp.substring(i, j));
                index++;
                i = j;
            } else if (isCharBracket(sExp.charAt(i)) || isCharOperator(sExp.charAt(i))) {
                String temp = sExp.substring(i, i + 1);
                //exceptional case, input "-" for negative number
                if (temp.equals(Operator.SUBTRACT) && index >= 1 && sData.get(index - 1).equals("(")) {
                    sData.add("0");
                    index++;
                }
                sData.add(temp);
                index++;
                i++;
            } else {
                throw new CalculateException(CalculateException.INVALID_CHARACTER);
            }
        }
    }

    /**
     * check the input number
     *
     * @throws CalculateException invalid number
     */
    private static void checkNumber() throws CalculateException {
        for (int i = 0; i < sData.size(); i++) {
            String string = sData.get(i);
            if (string.charAt(0) == '.' || string.charAt(string.length() - 1) == '.' || numOfChar(string, '.') > 1) {
                throw new CalculateException(CalculateException.INVALID_NUMBER);
            }
        }
    }

    /**
     * check bracket
     *
     * @throws CalculateException invalid bracket input
     */
    private static void checkBracket() throws CalculateException {
        sStack.clear();
        for (int i = 0; i < sData.size(); i++) {
            String string = sData.get(i);
            if (string.equals("(")) {
                sStack.push(string);
            } else if (string.equals(")")) {
                if (sStack.empty()) {
                    throw new CalculateException(CalculateException.INVALID_BRACKET);
                } else {
                    if (sStack.peek().equals("(")) {
                        sStack.pop();
                    } else {
                        throw new CalculateException(CalculateException.INVALID_BRACKET);
                    }
                }
            }
        }
        if (!sStack.empty()) {
            throw new CalculateException(CalculateException.INVALID_BRACKET);
        }
    }

    /**
     * check operator's input
     *
     * @throws CalculateException invalid operator order
     */
    private static void checkOperator() throws CalculateException {
        //check the first and last operator
        String string = sData.get(0);
        if (!string.equals("(") && !isCharNumber(string.charAt(0))) {
            throw new CalculateException(CalculateException.INVALID_OPERATOR);
        }
        string = sData.get(sData.size() - 1);
        if (!string.equals(")") && !isCharNumber(string.charAt(0)) && !string.equals(Operator.FACTORIAL)
                && !string.equals(Operator.PERCENT)) {
            throw new CalculateException(CalculateException.INVALID_OPERATOR);
        }
        //check the medium operators
        String lastString;
        String nextString;
        for (int i = 1; i < sData.size() - 1; i++) {
            string = sData.get(i);
            lastString = sData.get(i - 1);
            nextString = sData.get(i + 1);
            if (string.equals(Operator.FACTORIAL) || string.equals(Operator.PERCENT)) {
                if (!isCharNumber(lastString.charAt(0)) && !lastString.equals(")") && !lastString.equals(Operator.FACTORIAL)
                        && !lastString.equals(Operator.PERCENT)) {
                    throw new CalculateException(CalculateException.INVALID_OPERATOR);
                }
                if (isCharNumber(nextString.charAt(0)) || nextString.equals("(")) {
                    throw new CalculateException(CalculateException.INVALID_OPERATOR);
                }
            } else if (string.equals(Operator.ADD) || string.equals(Operator.SUBTRACT) || string.equals(Operator.MULTIPLY)
                    || string.equals(Operator.DIVIDE) || string.equals(Operator.POWER)) {
                if (!isCharNumber(lastString.charAt(0)) && !lastString.equals(")") && !lastString.equals(Operator.FACTORIAL)
                        && !lastString.equals(Operator.PERCENT)) {
                    throw new CalculateException(CalculateException.INVALID_OPERATOR);
                }
                if (!isCharNumber(nextString.charAt(0)) && !nextString.equals("(")) {
                    throw new CalculateException(CalculateException.INVALID_OPERATOR);
                }
            }
        }
    }

    /**
     * change to postfix expression
     */
    private static void changeToPostfixExp() {
        String string;
        sStack.clear();
        sQueue.clear();
        for (int i = 0; i < sData.size(); i++) {
            string = sData.get(i);
            if (isCharNumber(string.charAt(0))) {
                sQueue.offer(string);
            } else if (string.equals("(")) {
                sStack.push(string);
            } else if (string.equals(")")) {
                while (!sStack.peek().equals("(") && !sStack.empty()) {
                    sQueue.offer(sStack.pop());
                }
                if (!sStack.empty()) {
                    sStack.pop();
                }
            } else {
                if (sStack.empty() || sStack.peek().equals("(")
                        || Operator.getPriority(string) > Operator.getPriority(sStack.peek())) {
                    sStack.push(string);
                } else {
                    while (!sStack.empty() && !sStack.peek().equals("(")
                            && Operator.getPriority(string) <= Operator.getPriority(sStack.peek())) {
                        sQueue.offer(sStack.pop());
                    }
                    sStack.push(string);
                }
            }
        }
        while (!sStack.empty()) {
            sQueue.offer(sStack.pop());
        }
        sData.clear();
        while (!sQueue.isEmpty()) {
            sData.add(sQueue.poll());
        }
    }

    /**
     * get the result
     *
     * @return the string of result
     * @throws CalculateException calculate fails
     */
    private static String getResult() throws CalculateException {
        sStack.clear();
        String string;
        for (int i = 0; i < sData.size(); i++) {
            string = sData.get(i);
            if (isCharNumber(string.charAt(0))) {
                sStack.push(string);
            } else {
                double number = Double.parseDouble(sStack.pop());
                if (string.equals(Operator.FACTORIAL)) {
                    number = factorial(number);
                } else if (string.equals(Operator.PERCENT)) {
                    number = number / 100;
                } else {
                    double preNumber = Double.parseDouble(sStack.pop());
                    switch (string) {
                        case Operator.ADD:
                            number = preNumber + number;
                            break;
                        case Operator.SUBTRACT:
                            number = preNumber - number;
                            break;
                        case Operator.MULTIPLY:
                            number = preNumber * number;
                            break;
                        case Operator.DIVIDE:
                            if (number == 0) {
                                throw new CalculateException(CalculateException.INVALID_DIVIDE_ZERO);
                            } else {
                                number = preNumber / number;
                            }
                            break;
                        case Operator.POWER:
                            number = Math.pow(preNumber, number);
                            break;
                    }
                }
                sStack.push(number + "");
            }
        }
        String result;
        if (sStack.empty()) {
            throw new CalculateException(CalculateException.CALCULATE_ERROR);
        } else {
            result = sStack.pop();
            if (sSetDecimalPlaces) {
                StringBuilder pattern = new StringBuilder("0");
                if (sDecimalPlaces >= 1) {
                    pattern.append(".");
                    for (int i = 1; i <= sDecimalPlaces; i++) {
                        pattern.append("0");
                    }
                }
                result = new DecimalFormat(pattern.toString()).format(Double.parseDouble(result));
            } else {
                boolean isInteger = true;
                int index = result.indexOf(".");
                index++;
                for (; index < result.length(); index++) {
                    if (result.charAt(index) != '0') {
                        isInteger = false;
                        break;
                    }
                }
                if (isInteger) {
                    result = String.valueOf((int) Double.parseDouble(result));
                }
            }
            return result;
        }
    }

    private static boolean isCharNumber(char c) {
        return (c == '.' || (c >= '0' && c <= '9'));
    }

    private static boolean isCharBracket(char c) {
        return (c == '(' || c == ')');
    }

    private static boolean isCharOperator(char c) {
        return Operator.supportOperator(c + "");
    }

    private static int numOfChar(String string, char c) {
        int num = 0;
        if (string.length() == 0) {
            return num;
        }
        for (int i = 0; i < string.length(); i++) {
            if (string.charAt(i) == c) {
                num++;
            }
        }
        return num;
    }

    private static void init(String exp, int decimalPlaces) {
        sExp = exp;
        sDecimalPlaces = decimalPlaces;
        sData = new ArrayList<String>();
        sStack = new Stack<String>();
        sQueue = new LinkedList<String>();
    }

    private static void destroy() {
        sExp = null;
        sDecimalPlaces = DEFAULT_DECIMAL_PLACES;
        sSetDecimalPlaces = true;
        sData = null;
        sStack = null;
        sQueue = null;
    }

    public static double factorial(double number) throws CalculateException {
        if (number < 0) {
            throw new CalculateException(CalculateException.INVALID_FACTORIAL_NUMBER);
        }
        double result = 1;
        if (number == 0) {
            return result;
        }
        while (number > 0) {
            result *= number;
            number--;
        }
        return result;
    }
}